package il.ac.tau.cvws.scrabblesidekick.client.ui.view;

import il.ac.tau.cvws.scrabblesidekick.client.R;
import il.ac.tau.cvws.scrabblesidekick.client.app.controller.CameraController;
import il.ac.tau.cvws.scrabblesidekick.client.ui.view.camera.PictureTakenTask;
import il.ac.tau.cvws.scrabblesidekick.client.ui.view.dialog.ProgressDialogHandler;
import il.ac.tau.cvws.scrabblesidekick.shared.util.math.Point;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.LinearLayout;

/* The general camera view used by the calibration and game activities.
 * This view is responsible for displaying the camera preview and for taking
 * preview samples every constant interval, and passing them to the controller
 * for handling.
 */
@SuppressLint("ViewConstructor")
@TargetApi(14)
public class CameraLayoutView extends LinearLayout
{
	private static final int TIMER_RATE	= 1000;
	
	private final ByteArrayOutputStream _jpegCompressorBuffer;
	
	private final Context _context;
	private final CameraController _controller;	
 	private final ProgressDialogHandler _handler;
 	
 	private final int _height;
 	private final int _width;
 	
 	private SurfaceView _preview;
 	private SurfaceHolder _previewHolder;
 	private LayoutInflater _controlInflater;
 	
 	private Camera _camera;
 	
 	private Timer _cameraTimer; 	
 	private Point[] _border;
 	
 	private boolean _isPreview;
 	private boolean _cameraConfigured;

	public CameraLayoutView(CameraController contoller, Context context, ProgressDialogHandler handler, int width, int height)
	{
		super(context);
		
		_jpegCompressorBuffer = new ByteArrayOutputStream(); // for efficiency reasons - leave it as is!
		
		_controller = contoller;
		_context    = context;
		_handler    = handler;
		
		LayoutInflater inflater = LayoutInflater.from(context);
        inflater.inflate(R.layout.activity_calibration, this);
        
        _isPreview = false;
        
        _width     = width;
        _height    = height;
        
        init();
	}

	/* Deprecated, but mandatory for some reason.
	 */
 	@SuppressWarnings("deprecation")
	public void init()
 	{
 		_preview = (SurfaceView)findViewById(R.id.preview);
 		
 		_previewHolder = _preview.getHolder();
 		_previewHolder.addCallback(new SurfaceCallBack());
 		_previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
 		
 		_controlInflater = LayoutInflater.from(_context);
 	}
 	
 	/* Restart the timer which schedules acquiring preview images.
 	 */
 	public void restart()
 	{
 		if (_cameraTimer == null)
 		{
 			_cameraTimer = new Timer();
 		}
 		
		_cameraTimer.scheduleAtFixedRate(new TimerTask()
		{
			@Override
			public void run()
			{
				invokePreviewCallback();
			}
		}, 0, TIMER_RATE);
 	}
 	
 	/* Paint the preview border which directs the user, to the view.
 	 */
 	private void paintBorderOnScreen()
 	{
 		buildTiltedBorderLocation(); 		
 		
 		BorderView bTilt  = new BorderView(_controlInflater.getContext(), _border);
 		
 		LayoutParams layoutParamsControl = new LayoutParams(
 				ViewGroup.LayoutParams.MATCH_PARENT,
 				ViewGroup.LayoutParams.MATCH_PARENT);
 		
 		((Activity)getContext()).getWindow().addContentView(bTilt, layoutParamsControl);
 	}
 	
 	/* Builds the border location.
 	 * The border is located 1/12 screen height far from the top and bottom edges,
 	 * and is square shaped, to fit the Scrabble board.
 	 */
 	private void buildTiltedBorderLocation()
 	{
 		int midPointX = _width / 2;
 		
 		int y0 		  = (int)(_height / 12.0);
 		int rectWidth = (int)(_height - 2.0 * y0);
 		int x0 		  = (int)(midPointX - (rectWidth / 2.0));
 		
 		_border = new Point[]{new Point(x0, y0),
							  new Point(x0 + rectWidth, y0),
							  new Point(x0 + rectWidth, y0 + rectWidth),
							  new Point(x0, y0 + rectWidth)};

 	}
 	
 	/* Refreshes the view. Opens the camera if it isn't already open, and
 	 * starts the preview.
 	 */
 	public void refreshView()
 	{
 		if (_isPreview)
 		{
 			if (_camera == null)
 			{
 				_camera = Camera.open();	
 			}
 			
 			startPreview();
 		}
 	}
 	
 	/* Stops the camera and the preview, closes the timer, ending
 	 * the preview cycle.
 	 */
 	public void closeView()
 	{ 		
 		if (_isPreview)
 		{
 			_cameraTimer.cancel();
 	 		_cameraTimer.purge();
 	 		
 			_isPreview = false;
 			
 			_camera.stopPreview();
 			_camera.setPreviewCallback(null);
 			_camera.release();	
 			
 			_camera = null;
 		}
 	}
 	
 	/* The callback which is invoked by the timer at constant intervals. This
 	 * callback takes the preview image, creates a bitmap from it, sends it
 	 * to primary analysis in the client, and sends it to the server if the
 	 * primary analysis says so.
 	 */
	public void invokePreviewCallback()
	{
		if (_camera == null)
		{
			return;
		}
		
		/* Set the camera's preview callback. It is used to take a single picture,
		 * after which the preview callback shall be null'ified again.
		 */
		_camera.setPreviewCallback(new PreviewCallback() 
		{
            @Override
			public void onPreviewFrame(byte[] data, Camera camera) 
            {
            	/* Create a bitmap from the supplied image.
            	 */
    			byte[] imageByteArr = compressYuvToJpeg(data, _width, _height);
    	        Bitmap bitmap = BitmapFactory.decodeByteArray(imageByteArr, 0, imageByteArr.length);
    	        
    	        /* Send to controller in order to determine whether or not the camera is
    	         * positioned correctly.
    	         */
    	        boolean success = _controller.handleImagePosition(bitmap, _border);

            	_camera.setPreviewCallback(null);
            	
            	/* If positioned correctly, take a hi-res picture and let the controller handle it.
            	 */
    	        if (success)
    	        {
    	     		_cameraTimer.cancel();
    	     		
                	_camera.takePicture(null, null, new Camera.PictureCallback()
    				{                		
    					@Override
    					public void onPictureTaken(byte[] data, Camera camera)
    					{
    						Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
    						PictureTakenTask task = new PictureTakenTask(_width, _height, _context, _controller, _handler);
    						
    						task.execute(bitmap); 
    					}
    				});    	        	
    	        }
            }
            
            /* Compress the data we get from the camera to a JPEG we can work with.
             */
    		private byte[] compressYuvToJpeg(final byte[] yuvData, int width, int height) 
    		{
    		    _jpegCompressorBuffer.reset();
    		    
    		    YuvImage yuvImage = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
    		    yuvImage.compressToJpeg(new Rect(0, 0, width, height), 50, _jpegCompressorBuffer);
    		    
    		    return _jpegCompressorBuffer.toByteArray();
    		}
        });
	}
	
	/* Determine the best preview size. The best preview size is one which gives us the most
	 * image area, but does not span across more pixels than the surface, and which has a landscape
	 * orientation.
	 */
	private Camera.Size getBestPreviewSize(int width, int height, Camera.Parameters parameters) 
	{
		Camera.Size result = null;

		for (Camera.Size size : parameters.getSupportedPreviewSizes()) 
		{
			if ((size.width <= width) && (size.height <= height) && (size.height <= size.width)) 
			{	
				if (result == null) 
				{
					result = size;
				}
				else
				{
					int resultArea = result.width * result.height;
					int newArea    = size.width   * size.height;
		          
					if (newArea > resultArea) 
					{
						result = size;
					}
				}
			}
		}
    
		return result;
	}
	
	/* Initialize the camera preview. Retrieve the best supported size for the preview,
	 * set no-flash, and mark as configured.
	 */
  	private void initPreview(int width, int height) 
	{
  		if ((_camera == null) || (_previewHolder.getSurface() == null)) 
  		{
  			return;
  		}
  		
		try
		{
			_camera.setPreviewDisplay(_previewHolder);
		}
		catch (IOException e)
		{
			Log.e("camera", "Camera failed to load");
		}

		if (!_cameraConfigured) 
		{
			Camera.Parameters parameters =_camera.getParameters();
			Size previewSize = getBestPreviewSize(width, height, parameters);
			
			buildTiltedBorderLocation();
			
			if (previewSize != null) 
			{
				parameters.setPreviewSize(previewSize.width, previewSize.height);
				parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
				
				_camera.setParameters(parameters);
				_cameraConfigured = true;
			}
		}
	}
  
	private void startPreview() 
	{
		if ((_cameraConfigured) && (_camera != null)) 
		{
			_camera.startPreview();
			_isPreview = true;
		}
	}
  
	
	private class SurfaceCallBack implements SurfaceHolder.Callback 
	{
	    @Override
		public void surfaceCreated(SurfaceHolder holder) 
	    {
	    	// Do nothing
	    }
    
	    @Override
		public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) 
	    {
	    	_isPreview = true;
	    	
	    	refreshView();
	    	paintBorderOnScreen();
	    	initPreview(width, height);
	    	startPreview();
	    }
    
	    @Override
		public void surfaceDestroyed(SurfaceHolder holder) 
	    {
	    	// Do nothing
	    }
	}
}